Cloudflare Workers(Rust)からKVを使うチュートリアルをやってみた #Cloudflare
どうも!オペ部西村祐二です。
この記事は「Cloudflare のアドベントカレンダー」の4日目の記事です。
以前、下記のようなブログを書きました。そこからCloudflareのサービスに興味がでてきましたので今回、Cloudflare Workers(Rust)からCloudflare Workers KVを使うチュートリアルをやってみました。
今回、やってみたチュートリアルは下記になります。
Use Workers KV directly from Rust · Cloudflare Workers docs
Cloudflare Workers KVとは
Cloudflareのアプリケーション向けサーバーレス Key-Value ストレージです。
Cloudflare Workers KVは、Cloudflareのグローバルネットワーク内にあるすべてのデータセンターで、セキュアな低レイテンシーKey-Value Storeへのアクセスを提供。
Cloudflare Workers KV|サーバーレスコンピューティング | Cloudflare
やってみる
環境
- MacOS: 10.15.7
-
Rust: v1.58.0-nightly
-
Wrangler: 1.19.5
事前準備
- warnglerをインストール
wranglerというCLIをインストールしてCloudflareの環境を操作できるようにしておいてください。
詳細は下記参考サイトを確認ください。
cloudflare/wrangler: ? wrangle your Cloudflare Workers
Get started guide · Cloudflare Workers docs
雛形プロジェクトを作成
wranglerを使って雛形プロジェクトを作成します。
worker-rsを使用したテンプレートを指定します。
worker-rsはWebAssembly経由でRustで記述したプログラムをCloudflare Workersで動かすためのクレート(ライブラリ)です。
$ wrangler generate workers-kv-from-rust https://github.com/cloudflare/rustwasm-worker-template/ $ cd workers-kv-from-rust $ git add -A $ git commit -m 'Initial commit'
KV namespaceを作成
wranglerをつかって「KV_FROM_RUST」を指定し、namespaceを作成します。
プロジェクト名と指定した値を連結した「workers-kv-from-rust-KV_FROM_RUST」というnamespaceが作成されます。
❯ wrangler kv:namespace create "KV_FROM_RUST" ? Creating namespace with title "workers-kv-from-rust-KV_FROM_RUST" ✨ Success! Add the following to your configuration file: kv_namespaces = [ { binding = "KV_FROM_RUST", id = "xxxxxxxxxxxxxxxxxxx" }
また、--preview
をつけるとローカルから実行するときに参照されるKV namespaceを作成してくれます。
❯ wrangler kv:namespace create "KV_FROM_RUST" --preview ? Creating namespace with title "workers-kv-from-rust-KV_FROM_RUST_preview" ✨ Success! Add the following to your configuration file: kv_namespaces = [ { binding = "KV_FROM_RUST", preview_id = "xxxxxxxxxxxxxx" } ]
webコンソールを確認するとちゃんと作成されていることがわかります。
KV namespaceをバインドさせる
KV namespace作成時に生成されたidをwrangler.tomlファイルに追加します。
. . . kv_namespaces = [ { binding = "KV_FROM_RUST", preview_id = "xxxxxxxxxxxxx", id = "xxxxxxxxxx" } ] . . .
KV namespaceにアクセスするための下処理追加
JS側では変数KV_FROM_RUSTとして、作成したKV namespaceにアクセスします。Rustの名前空間から読み取りまたは書き込みを行うには、オブジェクト全体をRustハンドラー関数に渡す必要があります。
$ mkdir worker $ cd worker $ touch worker.js
addEventListener('fetch', event => { event.respondWith(handleRequest(event.request)) }) const { handle } = wasm_bindgen; const instance = wasm_bindgen(wasm); /** * Fetch and log a request * @param {Request} request */ async function handleRequest(request) { await instance; return await handle(KV_FROM_RUST, request); }
このチュートリアルでは、Rust側で可能な限り多くの処理を実行し、リクエストを直接wasmハンドラーに渡して、wasmハンドラーが応答を作成して返します。
リクエストとレスポンスの処理をJavaScript側で維持するテンプレートとは異なることに注意してください。
Rust側でなるべく処理を寄せるために、web-sysをRust依存関係の1つとして宣言し、Request、Response、およびResponseInitを明示的に有効にします。(UrlおよびUrlSearchParams機能はこのチュートリアルの後半で使用されます)。
. . . [dependencies.web-sys] version = "0.3" features = [ 'Request', 'Response', 'ResponseInit', 'Url', 'UrlSearchParams', ]
下記のように記述することでRustのRequest、Responseを使用して、常に200OKステータスで応答するシンプルなハンドラーを作成できます。
extern crate cfg_if; extern crate wasm_bindgen; mod utils; use cfg_if::cfg_if; use wasm_bindgen::{JsCast, prelude::*}; use web_sys::{Request, Response, ResponseInit}; cfg_if! { // When the `wee_alloc` feature is enabled, use `wee_alloc` as the global // allocator. if #[cfg(feature = "wee_alloc")] { extern crate wee_alloc; #[global_allocator] static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT; } } #[wasm_bindgen] pub fn handle(kv: JsValue, req: JsValue) -> Result<Response, JsValue> { let req: Request = req.dyn_into()?; let mut init = ResponseInit::new(); init.status(200); Response::new_with_opt_str_and_init(None, &init) }
wasm_bindgenを使用してKVにバインドする
先程のsrc/lib.rs
をベースに発展させていきます。
KV APIはJavaScriptのpromiseを返すため、依存関係としてwasm-bindgen-futures
とjs-sys
を追加する必要があります。
[dependencies] cfg-if = "0.1.2" wasm-bindgen = "=0.2.73" wasm-bindgen-futures = "0.4" js-sys = "0.3"
wasm_bindgen
を使用して型バインディングを作成してKVオブジェクトにアクセスするようにしたコードが下記になります。
. . . #[wasm_bindgen] pub fn handle(kv: WorkersKvJs, req: JsValue) -> Result<Response, JsValue> { let req: Request = req.dyn_into()?; let mut init = ResponseInit::new(); init.status(200); Response::new_with_opt_str_and_init(None, &init) } #[wasm_bindgen] extern "C" { pub type WorkersKvJs; #[wasm_bindgen(structural, method, catch)] pub async fn put( this: &WorkersKvJs, k: JsValue, v: JsValue, options: JsValue, ) -> Result<JsValue, JsValue>; #[wasm_bindgen(structural, method, catch)] pub async fn get( this: &WorkersKvJs, key: JsValue, options: JsValue, ) -> Result<JsValue, JsValue>; }
KVのラッパーを作成
KVパラメーターをそのまま使用し始めることもできますが、wasm_bindgenによって生成された関数シグネチャはRust内で機能するのが難しい場合があります。より簡単に扱うために、WorkersKvJs型の周りに単純な構造体を作成して、よりRustに適したAPIでラップします。
. . . use js_sys::{ArrayBuffer, Object, Reflect, Uint8Array}; struct WorkersKv { kv: WorkersKvJs, } impl WorkersKv { async fn put_text(&self, key: &str, value: &str, expiration_ttl: u64) -> Result<(), JsValue> { let options = Object::new(); Reflect::set(&options, &"expirationTtl".into(), &(expiration_ttl as f64).into())?; self.kv .put(JsValue::from_str(key), value.into(), options.into()) .await?; Ok(()) } async fn put_vec(&self, key: &str, value: &[u8], expiration_ttl: u64) -> Result<(), JsValue> { let options = Object::new(); Reflect::set(&options, &"expirationTtl".into(), &(expiration_ttl as f64).into())?; let typed_array = Uint8Array::new_with_length(value.len() as u32); typed_array.copy_from(value); self.kv .put( JsValue::from_str(key), typed_array.buffer().into(), options.into(), ) .await?; Ok(()) } async fn get_text(&self, key: &str) -> Result<Option<String>, JsValue> { let options = Object::new(); Reflect::set(&options, &"type".into(), &"text".into())?; Ok(self .kv .get(JsValue::from_str(key), options.into()) .await? .as_string()) } async fn get_vec(&self, key: &str) -> Result<Option<Vec<u8>>, JsValue> { let options = Object::new(); Reflect::set(&options, &"type".into(), &"arrayBuffer".into())?; let value = self.kv.get(JsValue::from_str(key), options.into()).await?; if value.is_null() { Ok(None) } else { let buffer = ArrayBuffer::from(value); let typed_array = Uint8Array::new_with_byte_offset(&buffer, 0); let mut v = vec![0; typed_array.length() as usize]; typed_array.copy_to(v.as_mut_slice()); Ok(Some(v)) } } }
上記のラッパーは、KV APIでサポートされているオプションのサブセットのみを利用可能にします。たとえば、putのexpirationTtlの代わりにexpirationや、getのtextやarrayBuffer以外のタイプなど、他のオプションも同様の方法でラップできます。概念的には、ラッパーメソッドはすべてReflect::set
を使用して手動でJSオブジェクトを作成し、必要に応じて戻り値を標準のRust型に変換します。
ラッパーを利用する処理を追加
ラッパーを使用してKV namespaceとの間で値を読み書きする準備が整いました。
次の関数は、URLセグメントを使用してKVドキュメントのキー名と値を決定し、PUTリクエストでKVに書き込むハンドラーの例です。たとえば、PUTリクエストを/foo?value=bar
に送信すると、「bar」値がfooキーに書き込まれます。
さらに、サンプルハンドラーは、GETリクエスト時に、URLパス名をキー名として使用してKVから読み取ります。たとえば、GET /foo
リクエストは、fooキーの値があればそれを返します。
. . . #[wasm_bindgen] pub async fn handle(kv: WorkersKvJs, req: JsValue) -> Result<Response, JsValue> { let req: Request = req.dyn_into()?; let url = web_sys::Url::new(&req.url())?; let pathname = url.pathname(); let query_params = url.search_params(); let kv = WorkersKv { kv }; match req.method().as_str() { "GET" => { let value = kv.get_text(&pathname).await?.unwrap_or_default(); let mut init = ResponseInit::new(); init.status(200); Response::new_with_opt_str_and_init(Some(&format!("\"{}\"\n", value)), &init) }, "PUT" => { let value = query_params.get("value").unwrap_or_default(); // set a TTL of 60 seconds: kv.put_text(&pathname, &value, 60).await?; let mut init = ResponseInit::new(); init.status(200); Response::new_with_opt_str_and_init(None, &init) }, _ => { let mut init = ResponseInit::new(); init.status(400); Response::new_with_opt_str_and_init(None, &init) } } }
src/lib.rsのコード全体
チュートリアルで紹介されたlib.rsのコードは下記のようになります。
KVにデータをPUTするときにTTL60秒が設定されて保存されるようになっているのでご注意ください。
extern crate cfg_if; extern crate wasm_bindgen; mod utils; use cfg_if::cfg_if; use js_sys::{ArrayBuffer, Object, Reflect, Uint8Array}; use wasm_bindgen::{prelude::*, JsCast}; use web_sys::{Request, Response, ResponseInit}; cfg_if! { // When the `wee_alloc` feature is enabled, use `wee_alloc` as the global // allocator. if #[cfg(feature = "wee_alloc")] { extern crate wee_alloc; #[global_allocator] static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT; } } #[wasm_bindgen] pub async fn handle(kv: WorkersKvJs, req: JsValue) -> Result<Response, JsValue> { let req: Request = req.dyn_into()?; let url = web_sys::Url::new(&req.url())?; let pathname = url.pathname(); let query_params = url.search_params(); let kv = WorkersKv { kv }; match req.method().as_str() { "GET" => { let value = kv.get_text(&pathname).await?.unwrap_or_default(); let mut init = ResponseInit::new(); init.status(200); Response::new_with_opt_str_and_init(Some(&format!("\"{}\"\n", value)), &init) } "PUT" => { let value = query_params.get("value").unwrap_or_default(); // set a TTL of 60 seconds: kv.put_text(&pathname, &value, 60).await?; let mut init = ResponseInit::new(); init.status(200); Response::new_with_opt_str_and_init(None, &init) } _ => { let mut init = ResponseInit::new(); init.status(400); Response::new_with_opt_str_and_init(None, &init) } } } #[wasm_bindgen] extern "C" { pub type WorkersKvJs; #[wasm_bindgen(structural, method, catch)] pub async fn put( this: &WorkersKvJs, k: JsValue, v: JsValue, options: JsValue, ) -> Result<JsValue, JsValue>; #[wasm_bindgen(structural, method, catch)] pub async fn get( this: &WorkersKvJs, key: JsValue, options: JsValue, ) -> Result<JsValue, JsValue>; } struct WorkersKv { kv: WorkersKvJs, } impl WorkersKv { async fn put_text(&self, key: &str, value: &str, expiration_ttl: u64) -> Result<(), JsValue> { let options = Object::new(); Reflect::set(&options, &"expirationTtl".into(), &(expiration_ttl as f64).into())?; self.kv .put(JsValue::from_str(key), value.into(), options.into()) .await?; Ok(()) } async fn put_vec(&self, key: &str, value: &[u8], expiration_ttl: u64) -> Result<(), JsValue> { let options = Object::new(); Reflect::set(&options, &"expirationTtl".into(), &(expiration_ttl as f64).into())?; let typed_array = Uint8Array::new_with_length(value.len() as u32); typed_array.copy_from(value); self.kv .put( JsValue::from_str(key), typed_array.buffer().into(), options.into(), ) .await?; Ok(()) } async fn get_text(&self, key: &str) -> Result<Option<String>, JsValue> { let options = Object::new(); Reflect::set(&options, &"type".into(), &"text".into())?; Ok(self .kv .get(JsValue::from_str(key), options.into()) .await? .as_string()) } async fn get_vec(&self, key: &str) -> Result<Option<Vec<u8>>, JsValue> { let options = Object::new(); Reflect::set(&options, &"type".into(), &"arrayBuffer".into())?; let value = self.kv.get(JsValue::from_str(key), options.into()).await?; if value.is_null() { Ok(None) } else { let buffer = ArrayBuffer::from(value); let typed_array = Uint8Array::new_with_byte_offset(&buffer, 0); let mut v = vec![0; typed_array.length() as usize]; typed_array.copy_to(v.as_mut_slice()); Ok(Some(v)) } } }
ローカルで動作確認
CLIを使ってローカルサーバを実行してみます。
するとエラーとなってしまいました。
$ wrangler dev . . . [INFO]: ⬇️ Installing wasm-bindgen... [INFO]: Optimizing wasm binaries with `wasm-opt`... [INFO]: Optional fields missing from Cargo.toml: 'description', 'repository', and 'license'. These are not necessary, but recommended [INFO]: ✨ Done in 1.31s [INFO]: ? Your wasm pkg is ready to publish at /Users/nishimura.yuji/study/cloudflare/kv/workers-kv-from-rust/build. Error: Something went wrong with the request to Cloudflare... Uncaught SyntaxError: The requested module './index_bg.mjs' does not provide an export named 'fetch' at line 2 [API code: 10021]
エラー修正
wrangler.toml
の2行がjavascript
になっていたので、rust
に修正します。
name = "workers-kv-from-rust" type = "rust" workers_dev = true compatibility_date = "2021-12-03" kv_namespaces = [ . . .
再度、ローカルで動作確認
CLIを使ってローカルサーバを実行してみます。
一応エラーが出ずにロールサーバがたちあがりました。いくつか警告がでていますがチュートリアルということで今回は無視して進めます。
$ wrangler dev . . . [INFO]: ⬇️ Installing wasm-bindgen... [INFO]: Optimizing wasm binaries with `wasm-opt`... [INFO]: Optional fields missing from Cargo.toml: 'description', 'repository', and 'license'. These are not necessary, but recommended [INFO]: ✨ Done in 0.53s [INFO]: ? Your wasm pkg is ready to publish at /Users/nishimura.yuji/study/cloudflare/kv/workers-kv-from-rust/pkg. [WARN]: ⚠️ There's a newer version of wasm-pack available, the new version is: 0.10.1, you are using: 0.10.0. To update, navigate to: https://rustwasm.github.io/wasm-pack/installer/ ? watching "./" ? Listening on http://127.0.0.1:8787
curlを使って動作確認してみます。
$ curl 'localhost:8787/foo' "" $ curl -X PUT 'localhost:8787/foo?value=bar' $ curl 'localhost:8787/foo' "bar"
問題なく動作しているようです。
また、TTLを60秒で設定しているので、時間が経過するとKVからデータが消えていることも確認できました。
webコンソールから確認してみます。
末尾に「preview」のKV namespaceの中にデータがあることが確認できました。
デプロイ
チュートリアルでは記載はないですが、実際にデプロイして動作確認もしてみます。
下記コマンドで簡単にデプロイできます。
❯ wrangler publish ? Compiling your project to WebAssembly... [INFO]: ? Checking for the Wasm target... [INFO]: ? Compiling to Wasm... [INFO]: ⬇️ Installing wasm-bindgen... [INFO]: Optimizing wasm binaries with `wasm-opt`... . . . ✨ Build succeeded ✨ Successfully published your script to https://<your url>
curlで動作確認してみます。
今度はJSON形式のデータの{"name":"foo","selected":[1,2,3],"flags":{"a":true,"b":false}}
を保存します。
jsonをurlにパスに変換する必要があります。
$ curl -X PUT 'https://<your url>/foo2?value=%7B%22name%22%3A%22foo%22%2C%22selected%22%3A%5B1%2C2%2C3%5D%2C%22flags%22%3A%7B%22a%22%3Atrue%2C%22b%22%3Afalse%7D%7D' $ curl 'https://<your url>/foo2' "{"name":"foo","selected":[1,2,3],"flags":{"a":true,"b":false}}"
webコンソールからもきちんとデータが保存されていることが確認できます。
また KV namespaceはpreview
がついていないnamespaceに保存されています。
さいごに
Cloudflare Workers(Rust)からCloudflare Workers KVを使うチュートリアルをやってみました。
Cloudflare WorkersがV8を直接実行できる環境ということもあって、RustとWebAssemblyの知識、wasm-bindgenの使い方などの知識が必要となり、なかなかハードルを感じましたがとても楽しく、勉強になるチュートリアルでした。
誰かの参考になれば幸いです。
参考サイト
Rust から WebAssembly にコンパイルする - WebAssembly | MDN
wasm-pack で JS の Promise を await できる非同期 Rust を書いて node.js で動かす - Qiita
fkettelhoit/workers-kv-from-rust: Example Cloudflare Worker that calls Workers KV directly from Rust